 /*******************************************************************************
  * Copyright (c) 2004, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.osgi.storagemanager;

 import java.io.*;
 import java.security.AccessController ;
 import java.util.*;
 import org.eclipse.core.runtime.internal.adaptor.*;
 import org.eclipse.osgi.framework.internal.reliablefile.*;
 import org.eclipse.osgi.framework.util.SecureAction;

 /**
  * Storage managers provide a facility for tracking the state of a group of files having
  * relationship with each others and being updated by several entities at the same time.
  * The typical usecase is in shared configuration data areas.
  * <p>
  * The facilities provided here are cooperative. That is, all participants must
  * agree to the conventions and to calling the given API. There is no capacity
  * to enforce these conventions or prohibit corruption.
  * </p>
  * <p>
  * Clients can not extend this class
  * </p>
  * <p>
  * Example
  * <pre>
  * //Open the storage manager
  * org.eclipse.osgi.storagemanager.StorageManager cacheStorageManager = new StorageManager("d:/sharedFolder/bar/", false); //$NON-NLS-1$
  * try {
  * cacheStorageManager.open(true);
  * } catch (IOException e) {
  * // Ignore the exception. The registry will be rebuilt from source.
  * }
  *
  * //To read from a file
  * java.io.File fileA = cacheStorageManager.lookup("fileA", false));
  * java.io.File fileB = cacheStorageManager.lookup("fileB", false));
  * //Do the reading code
  * new java.io.FileOutputStream(fileA);
  *
  * //To write in files
  * cacheStorageManager.add("fileC"); //add the file to the filemanager (in this case we assume it is not already here)
  * cacheStorageManager.add("fileD");
  *
  * // The file is never written directly into the file name, so we create some temporary file
  * java.io.File fileC = cacheStorageManager.createTempFile("fileC");
  * java.io.File fileD = cacheStorageManager.createTempFile("fileD");
  *
  * //Do the actual writing here...
  *
  * //Finally update the storagemanager with the actual file to manage.
  * cacheStorageManager.update(new String[] {"fileC", "fileD"}, new String[] {fileC.getName(), fileD.getName()};
  *
  * //Close the file manager at the end
  * cacheStorageManager.close();
  * </pre>
  * </p>
  * <p>
  * Implementation details <br>
  * The following implementation details are provided to help with understanding the
  * behavior of this class.
  * The general principle is to maintain a table which maps user-level file names
  * onto an actual disk files. If a file needs to be modified,
  * it is stored into a new file. The old content is not removed from disk until all entities
  * have closed there instance of the storage manager.
  * Once the instance has been created, open() must be called before performing any other operation.
  * On open the storage manager obtains a snapshot of the current managed files contents. If an
  * entity updates a managed file, the storage manager will save the content for that instance of the
  * storage manager, all other storage manager instances will still have access to that managed file's
  * content as it was when the instance was first opened.
  * </p>
  * @since 3.2
  */

 // Note the implementation of this class originated from the following deprecated classes:
 // /org.eclipse.osgi/eclipseAdaptor/src/org/eclipse/core/runtime/adaptor/FileManager.java
 // /org.eclipse.osgi/eclipseAdaptor/src/org/eclipse/core/runtime/adaptor/StreamManager.java
 public final class StorageManager {
     private static final int FILETYPE_STANDARD = 0;
     private static final int FILETYPE_RELIABLEFILE = 1;
     private static final SecureAction secure = (SecureAction) AccessController.doPrivileged(SecureAction.createSecureAction());
     private static boolean tempCleanup = Boolean.valueOf(secure.getProperty("osgi.embedded.cleanTempFiles")).booleanValue(); //$NON-NLS-1$
 private static boolean openCleanup = Boolean.valueOf(secure.getProperty("osgi.embedded.cleanupOnOpen")).booleanValue(); //$NON-NLS-1$
 private static final String MANAGER_FOLDER = ".manager"; //$NON-NLS-1$
 private static final String TABLE_FILE = ".fileTable"; //$NON-NLS-1$
 private static final String LOCK_FILE = ".fileTableLock"; //$NON-NLS-1$
 private static final int MAX_LOCK_WAIT = 5000; // 5 seconds

     private class Entry {
         int readId;
         int writeId;
         int fileType;

         Entry(int readId, int writeId, int type) {
             this.readId = readId;
             this.writeId = writeId;
             this.fileType = type;
         }

         int getReadId() {
             return readId;
         }

         int getWriteId() {
             return writeId;
         }

         int getFileType() {
             return fileType;
         }

         void setReadId(int value) {
             readId = value;
         }

         void setWriteId(int value) {
             writeId = value;
         }

         void setFileType(int type) {
             fileType = type;
         }
     }

     private File base; //The folder managed
 private File managerRoot; //The folder that will contain all the file related to the functionning of the manager (typically a subdir of base)

     private String lockMode = null;
     private File tableFile = null;
     private File lockFile; // The lock file for the table (this file is the same for all the instances)
 private Locker locker; // The locker for the lock

     private File instanceFile = null; //The file reprensenting the running instance. It is created when the table file is read.
 private Locker instanceLocker = null; //The locker for the instance file.
 private boolean readOnly; // Whether this storage manager is in read-only mode
 private boolean open; // Whether this storage manager is open for use

     // locking related fields
 private int tableStamp = -1;

     private Properties table = new Properties();
     private boolean useReliableFiles = Boolean.valueOf(secure.getProperty("osgi.useReliableFiles")).booleanValue(); //$NON-NLS-1$

     /**
      * Returns a new storage manager for the area identified by the given base
      * directory.
      *
      * @param base the directory holding the files to be managed
      * @param lockMode the lockMode to use for the storage manager. It can have one the 3 values: none, java.io, java.nio
      * and also supports null in which case the lock strategy will be the global one.
      */
     public StorageManager(File base, String lockMode) {
         this(base, lockMode, false);
     }

     /**
      * Returns a new storage manager for the area identified by the given base
      * directory.
      *
      * @param base the directory holding the files to be managed
      * @param lockMode the lockMode to use for the storage manager. It can have one the 3 values: none, java.io, java.nio
      * and also supports null in which case the lock strategy will be the global one.
      * @param readOnly true if the managed files are read-only
      */
     public StorageManager(File base, String lockMode, boolean readOnly) {
         this.base = base;
         this.lockMode = lockMode;
         this.managerRoot = new File(base, MANAGER_FOLDER);
         if (!readOnly)
             this.managerRoot.mkdirs();
         this.tableFile = new File(managerRoot, TABLE_FILE);
         this.lockFile = new File(managerRoot, LOCK_FILE);
         this.readOnly = readOnly;
         open = false;
     }

     private void initializeInstanceFile() throws IOException {
         if (instanceFile != null || readOnly)
             return;
         this.instanceFile = File.createTempFile(".tmp", ".instance", managerRoot); //$NON-NLS-1$//$NON-NLS-2$
 this.instanceFile.deleteOnExit();
         instanceLocker = BasicLocation.createLocker(instanceFile, lockMode);
         instanceLocker.lock();
     }

     private String getAbsolutePath(String file) {
         return new File(base, file).getAbsolutePath();
     }

     /**
      * Add the given managed file name to the list of files managed by this manager.
      *
      * @param managedFile name of the file to manage
      * @throws IOException if there are any problems adding the given file name to the manager
      */
     public void add(String managedFile) throws IOException {
         add(managedFile, FILETYPE_STANDARD);
     }

     /* (non-Javadoc
      * Add the given file name to the list of files managed by this manager.
      *
      * @param managedFile name of the file to manage.
      * @param fileType the file type.
      * @throws IOException if there are any problems adding the given file to the manager
      */
     private void add(String managedFile, int fileType) throws IOException {
         if (!open)
             throw new IOException(EclipseAdaptorMsg.fileManager_notOpen);
         if (readOnly)
             throw new IOException(EclipseAdaptorMsg.fileManager_illegalInReadOnlyMode);
         if (!lock(true))
             throw new IOException(EclipseAdaptorMsg.fileManager_cannotLock);
         try {
             updateTable();
             Entry entry = (Entry) table.get(managedFile);
             if (entry == null) {
                 entry = new Entry(0, 1, fileType);
                 table.put(managedFile, entry);
                 // if this managed file existed before, ensure there is not an old
 // version on the disk to avoid name collisions. If version found,
 // us the oldest generation+1 for the write ID.
 int oldestGeneration = findOldestGeneration(managedFile);
                 if (oldestGeneration != 0)
                     entry.setWriteId(oldestGeneration + 1);
                 save();
             } else {
                 if (entry.getFileType() != fileType) {
                     entry.setFileType(fileType);
                     updateTable();
                     save();
                 }
             }
         } finally {
             release();
         }
     }

     /* (non-Javadoc)
      * Find the oldest generation of a file still available on disk
      * @param file the file from which to obtain the oldest generation.
      * @return the oldest generation of the file or 0 if the file does
      * not exist.
      */
     private int findOldestGeneration(String managedFile) {
         String [] files = base.list();
         int oldestGeneration = 0;
         if (files != null) {
             String name = managedFile + '.';
             int len = name.length();
             for (int i = 0; i < files.length; i++) {
                 if (!files[i].startsWith(name))
                     continue;
                 try {
                     int generation = Integer.parseInt(files[i].substring(len));
                     if (generation > oldestGeneration)
                         oldestGeneration = generation;
                 } catch (NumberFormatException e) {
                     continue;
                 }
             }
         }
         return oldestGeneration;
     }

     /**
      * Update the given managed files with the content in the given source files.
      * The managedFiles is a list of managed file names which are currently managed.
      * If a managed file name is not currently managed it will be added as a
      * managed file for this storage manager.
      * The sources are absolute (or relative to the current working directory)
      * file paths containing the new content for the corresponding managed files.
      *
      * @param managedFiles the managed files to update
      * @param sources the new content for the managed files
      * @throws IOException if there are any problems updating the given managed files
      */
     public void update(String [] managedFiles, String [] sources) throws IOException {
         if (!open)
             throw new IOException(EclipseAdaptorMsg.fileManager_notOpen);
         if (readOnly)
             throw new IOException(EclipseAdaptorMsg.fileManager_illegalInReadOnlyMode);
         if (!lock(true))
             throw new IOException(EclipseAdaptorMsg.fileManager_cannotLock);
         try {
             updateTable();
             int[] originalReadIDs = new int[managedFiles.length];
             boolean error = false;
             for (int i = 0; i < managedFiles.length; i++) {
                 originalReadIDs[i] = getId(managedFiles[i]);
                 if (!update(managedFiles[i], sources[i]))
                     error = true;
             }
             if (error) {
                 // restore the original readIDs to avoid inconsistency for this group
 for (int i = 0; i < managedFiles.length; i++) {
                     Entry entry = (Entry) table.get(managedFiles[i]);
                     entry.setReadId(originalReadIDs[i]);
                 }
                 throw new IOException(EclipseAdaptorMsg.fileManager_updateFailed);
             }
             save(); //save only if no errors
 } finally {
             release();
         }
     }

     /**
      * Returns a list of all the managed files currently being managed.
      *
      * @return the names of the managed files
      */
     public String [] getManagedFiles() {
         if (!open)
             return null;
         Set set = table.keySet();
         String [] keys = (String []) set.toArray(new String [set.size()]);
         String [] result = new String [keys.length];
         for (int i = 0; i < keys.length; i++)
             result[i] = new String (keys[i]);
         return result;
     }

     /**
      * Returns the directory containing the files being managed by this storage
      * manager.
      *
      * @return the directory containing the managed files
      */
     public File getBase() {
         return base;
     }

     /**
      * Returns the current numeric id (appendage) of the given managed file.
      * <code>managedFile + "." + getId(target)</code>. A value of -1 is returned
      * if the given name is not managed.
      *
      * @param managedFile the name of the managed file
      * @return the id of the managed file
      */
     public int getId(String managedFile) {
         if (!open)
             return -1;
         Entry entry = (Entry) table.get(managedFile);
         if (entry == null)
             return -1;
         return entry.getReadId();
     }

     /**
      * Returns if readOnly state this storage manager is using.
      *
      * @return if this storage manager update state is read-only.
      */
     public boolean isReadOnly() {
         return readOnly;
     }

     /* (non-Javadoc)
      * Attempts to lock the state of this manager and returns <code>true</code>
      * if the lock could be acquired.
      * <p>
      * Locking a manager is advisory only. That is, it does not prevent other
      * applications from modifying the files managed by this manager.
      * </p>
      *
      * @exception IOException
      * if there was an unexpected problem while acquiring the
      * lock.
      */
     private boolean lock(boolean wait) throws IOException {
         if (readOnly)
             return false;
         if (locker == null) {
             locker = BasicLocation.createLocker(lockFile, lockMode);
             if (locker == null)
                 throw new IOException(EclipseAdaptorMsg.fileManager_cannotLock);
         }
         boolean locked = locker.lock();
         if (locked || !wait)
             return locked;
         //Someone else must have the directory locked, but they should release it quickly
 long start = System.currentTimeMillis();
         while (true) {
             try {
                 Thread.sleep(200); // 5x per second
 } catch (InterruptedException e) {/*ignore*/
             }
             locked = locker.lock();
             if (locked)
                 return true;
             // never wait longer than 5 seconds
 long time = System.currentTimeMillis() - start;
             if (time > MAX_LOCK_WAIT)
                 return false;
         }
     }

     /**
      * Returns the actual file location to use when reading the given managed file.
      * A value of <code>null</code> can be returned if the given managed file name is not
      * managed and add is set to false.
      * <p>
      * The returned file should be considered read-only. Any updates to the content of this
      * file should be done using {@link #update(String[], String[])}.
      *
      * @param managedFile the managed file to lookup
      * @param add indicate whether the managed file name should be added to the manager if
      * it is not already managed.
      * @throws IOException if the add flag is set to true and the addition of the managed file failed
      * @return the absolute file location to use for the given managed file or
      * <code>null</code> if the given managed file is not managed
      */
     public File lookup(String managedFile, boolean add) throws IOException {
         if (!open)
             throw new IOException(EclipseAdaptorMsg.fileManager_notOpen);
         Entry entry = (Entry) table.get(managedFile);
         if (entry == null) {
             if (add) {
                 add(managedFile);
                 entry = (Entry) table.get(managedFile);
             } else {
                 return null;
             }
         }
         return new File(getAbsolutePath(managedFile + '.' + entry.getReadId()));
     }

     private boolean move(String source, String managedFile) {
         File original = new File(source);
         File targetFile = new File(managedFile);
         // its ok if the original does not exist. The table entry will capture
 // that fact. There is no need to put something in the filesystem.
 if (!original.exists() || targetFile.exists())
             return false;
         return original.renameTo(targetFile);
     }

     /**
      * Saves the state of the storage manager and releases any locks held.
      */
     private void release() {
         if (locker == null)
             return;
         locker.release();
     }

     /**
      * Removes the given managed file from management by this storage manager.
      *
      * @param managedFile the managed file to remove
      * @throws IOException if an error occured removing the managed file
      */
     public void remove(String managedFile) throws IOException {
         if (!open)
             throw new IOException(EclipseAdaptorMsg.fileManager_notOpen);
         if (readOnly)
             throw new IOException(EclipseAdaptorMsg.fileManager_illegalInReadOnlyMode);
         // The removal needs to be done eagerly, so the value is effectively removed from the disktable.
 // Otherwise, an updateTable() caused by an update(,) could cause the file to readded to the local table.
 if (!lock(true))
             throw new IOException(EclipseAdaptorMsg.fileManager_cannotLock);
         try {
             updateTable();
             table.remove(managedFile);
             save();
         } finally {
             release();
         }
     }

     private void updateTable() throws IOException {
         int stamp;
         stamp = ReliableFile.lastModifiedVersion(tableFile);
         if (stamp == tableStamp || stamp == -1)
             return;
         Properties diskTable = new Properties();
         try {
             InputStream input;
             input = new ReliableFileInputStream(tableFile);
             try {
                 diskTable.load(input);
             } finally {
                 input.close();
             }
         } catch (IOException e) {
             throw e; // rethrow the exception, we have nothing to add here
 }
         tableStamp = stamp;
         for (Enumeration e = diskTable.keys(); e.hasMoreElements();) {
             String file = (String ) e.nextElement();
             String value = diskTable.getProperty(file);
             if (value != null) {
                 Entry entry = (Entry) table.get(file);
                 // check front of value for ReliableFile
 int id;
                 int fileType;
                 int idx = value.indexOf(',');
                 if (idx != -1) {
                     id = Integer.parseInt(value.substring(0, idx));
                     fileType = Integer.parseInt(value.substring(idx + 1));
                 } else {
                     id = Integer.parseInt(value);
                     fileType = FILETYPE_STANDARD;
                 }
                 if (entry == null) {
                     table.put(file, new Entry(id, id + 1, fileType));
                 } else {
                     entry.setWriteId(id + 1);
                     //don't change type
 }
             }
         }
     }

     /*
      * This method should be called while the manager is locked.
      */
     private void save() throws IOException {
         if (readOnly)
             return;
         // if the table file has change on disk, update our data structures then
 // rewrite the file.
 updateTable();

         Properties props = new Properties();
         for (Enumeration e = table.keys(); e.hasMoreElements();) {
             String file = (String ) e.nextElement();
             Entry entry = (Entry) table.get(file);
             String value;
             if (entry.getFileType() != FILETYPE_STANDARD) {
                 value = Integer.toString(entry.getWriteId() - 1) + ',' + //In the table we save the write number - 1, because the read number can be totally different.
 Integer.toString(entry.getFileType());
             } else {
                 value = Integer.toString(entry.getWriteId() - 1); //In the table we save the write number - 1, because the read number can be totally different.
 }
             props.put(file, value);
         }
         ReliableFileOutputStream fileStream = new ReliableFileOutputStream(tableFile);
         try {
             boolean error = true;
             try {
                 props.store(fileStream, "safe table"); //$NON-NLS-1$
 fileStream.close();
                 error = false;
             } finally {
                 if (error)
                     fileStream.abort();
             }
         } catch (IOException e) {
             throw new IOException(EclipseAdaptorMsg.fileManager_couldNotSave);
         }
         tableStamp = ReliableFile.lastModifiedVersion(tableFile);
     }

     private boolean update(String managedFile, String source) throws IOException {
         Entry entry = (Entry) table.get(managedFile);
         if (entry == null)
             add(managedFile);
         int newId = entry.getWriteId();
         // attempt to rename the file to the next generation
 boolean success = move(getAbsolutePath(source), getAbsolutePath(managedFile) + '.' + newId);
         if (!success) {
             //possible the next write generation file exists? Lets determine the largest
 //generation number, then use that + 1.
 newId = findOldestGeneration(managedFile) + 1;
             success = move(getAbsolutePath(source), getAbsolutePath(managedFile) + '.' + newId);
         }
         if (!success)
             return false;
         // update the entry. read and write ids should be the same since
 // everything is in sync
 entry.setReadId(newId);
         entry.setWriteId(newId + 1);
         return true;
     }

     /**
      * This methods remove all the temporary files that have been created by the storage manager.
      * This removal is only done if the instance of eclipse calling this method is the last instance using this storage manager.
      * @throws IOException
      */
     private void cleanup() throws IOException {
         if (readOnly)
             return;
         //Lock first, so someone else can not start while we're in the middle of cleanup
 if (!lock(true))
             throw new IOException(EclipseAdaptorMsg.fileManager_cannotLock);
         try {
             //Iterate through the temp files and delete them all, except the one representing this storage manager.
 String [] files = managerRoot.list();
             if (files != null) {
                 for (int i = 0; i < files.length; i++) {
                     if (files[i].endsWith(".instance") && instanceFile != null && !files[i].equalsIgnoreCase(instanceFile.getName())) { //$NON-NLS-1$
 Locker tmpLocker = BasicLocation.createLocker(new File(managerRoot, files[i]), lockMode);
                         if (tmpLocker.lock()) {
                             //If I can lock it is a file that has been left behind by a crash
 tmpLocker.release();
                             new File(managerRoot, files[i]).delete();
                         } else {
                             tmpLocker.release();
                             return; //The file is still being locked by somebody else
 }
                     }
                 }
             }

             //If we are here it is because we are the last instance running. After locking the table and getting its latest content, remove all the backup files and change the table
 updateTable();
             Collection managedFiles = table.entrySet();
             for (Iterator iter = managedFiles.iterator(); iter.hasNext();) {
                 Map.Entry fileEntry = (Map.Entry) iter.next();
                 String fileName = (String ) fileEntry.getKey();
                 Entry info = (Entry) fileEntry.getValue();
                 if (info.getFileType() == FILETYPE_RELIABLEFILE) {
                     ReliableFile.cleanupGenerations(new File(base, fileName));
                 } else {
                     //Because we are cleaning up, we are giving up the values from our table, and we must delete all the files that are not referenced by the table
 String readId = Integer.toString(info.getWriteId() - 1);
                     deleteCopies(fileName, readId);
                 }
             }

             if (tempCleanup) {
                 files = base.list();
                 if (files != null) {
                     for (int i = 0; i < files.length; i++) {
                         if (files[i].endsWith(ReliableFile.tmpExt)) {
                             new File(base, files[i]).delete();
                         }
                     }
                 }
             }
         } catch (IOException e) {
             //If the exception comes from the updateTable(), there has been a problem in reading the file.
 //If an exception occured in the save, then the table won't be up to date!
 throw e;
         } finally {
             release();
         }
     }

     private void deleteCopies(String fileName, String exceptionNumber) {
         String notToDelete = fileName + '.' + exceptionNumber;
         String [] files = base.list();
         if (files == null)
             return;
         for (int i = 0; i < files.length; i++) {
             if (files[i].startsWith(fileName + '.') && !files[i].equals(notToDelete))
                 new File(base, files[i]).delete();
         }
     }

     /**
      * This method declares the storage manager as closed. From thereon, the instance can no longer be used.
      * It is important to close the manager as it also cleans up old copies of the managed files.
      */
     public void close() {
         if (!open)
             return;
         open = false;
         if (readOnly)
             return;
         try {
             cleanup();
         } catch (IOException e) {
             //Ignore and close.
 }
         if (instanceLocker != null)
             instanceLocker.release();

         if (instanceFile != null)
             instanceFile.delete();
     }

     /**
      * This methods opens the storage manager.
      * This method must be called before any operation on the storage manager.
      * @param wait indicates if the open operation must wait in case of contention on the lock file.
      * @throws IOException if an error occured opening the storage manager
      */
     public void open(boolean wait) throws IOException {
         if (openCleanup)
             cleanup();
         if (!readOnly) {
             boolean locked = lock(wait);
             if (!locked && wait)
                 throw new IOException(EclipseAdaptorMsg.fileManager_cannotLock);
         }

         try {
             initializeInstanceFile();
             updateTable();
             open = true;
         } finally {
             release();
         }
     }

     /**
      * Creates a new unique empty temporary-file in the storage manager base directory. The file name
      * must be at least 3 characters. This file can later be used to update a managed file.
      *
      * @param file the file name to create temporary file from.
      * @return the newly-created empty file.
      * @throws IOException if the file can not be created.
      * @see #update(String[], String[])
      */
     public File createTempFile(String file) throws IOException {
         if (readOnly)
             throw new IOException(EclipseAdaptorMsg.fileManager_illegalInReadOnlyMode);
         File tmpFile = File.createTempFile(file, ReliableFile.tmpExt, base);
         tmpFile.deleteOnExit();
         return tmpFile;
     }

     /**
      * Returns a managed <code>InputStream</code> for a managed file.
      * <code>null</code> can be returned if the given name is not managed.
      *
      * @param managedFile the name of the managed file to open.
      * @return an input stream to the managed file or
      * <code>null</code> if the given name is not managed.
      * @throws IOException if the content is missing, corrupt or an error occurs.
      */
     public InputStream getInputStream(String managedFile) throws IOException {
         return getInputStream(managedFile, ReliableFile.OPEN_BEST_AVAILABLE);
     }

     /**
      * Returns a managed input stream set for the managed file names.
      * Elements of the returned set may be <code>null</code> if a given name is not managed.
      * This method should be used for managed file sets which use the output streams returned
      * by the {@link #getOutputStreamSet(String[])} to save data.
      *
      * @param managedFiles the names of the managed files to open.
      * @return a set input streams to the given managed files.
      * @throws IOException if the content of one of the managed files is missing, corrupt or an error occurs.
      */
     public InputStream[] getInputStreamSet(String [] managedFiles) throws IOException {
         InputStream[] streams = new InputStream[managedFiles.length];
         for (int i = 0; i < streams.length; i++)
             streams[i] = getInputStream(managedFiles[i], ReliableFile.OPEN_FAIL_ON_PRIMARY);
         return streams;
     }

     private InputStream getInputStream(String managedFiles, int openMask) throws IOException {
         if (useReliableFiles) {
             int id = getId(managedFiles);
             if (id == -1)
                 return null;
             return new ReliableFileInputStream(new File(getBase(), managedFiles), id, openMask);
         }
         File lookup = lookup(managedFiles, false);
         if (lookup == null)
             return null;
         return new FileInputStream(lookup);
     }

     /**
      * Returns a <code>ManagedOutputStream</code> for a managed file.
      * Closing the ouput stream will update the storage manager with the
      * new content of the managed file.
      *
      * @param managedFile the name of the managed file to write.
      * @return a managed output stream for the managed file.
      * @throws IOException if an error occurs opening the managed file.
      */
     public ManagedOutputStream getOutputStream(String managedFile) throws IOException {
         if (useReliableFiles) {
             ReliableFileOutputStream out = new ReliableFileOutputStream(new File(getBase(), managedFile));
             return new ManagedOutputStream(out, this, managedFile, null);
         }
         File tmpFile = createTempFile(managedFile);
         return new ManagedOutputStream(new FileOutputStream(tmpFile), this, managedFile, tmpFile);
     }

     /**
      * Returns an array of <code>ManagedOutputStream</code> for a set of managed files.
      * When all managed output streams in the set have been closed, the storage manager
      * will be updated with the new content of the managed files.
      * Aborting any one of the streams will cause the entire content of the set to abort
      * and be discarded.
      *
      * @param managedFiles list of names of the managed file to write.
      * @return an array of managed output streams respectively of managed files.
      * @throws IOException if an error occurs opening the managed files.
      */
     public ManagedOutputStream[] getOutputStreamSet(String [] managedFiles) throws IOException {
         int count = managedFiles.length;
         ManagedOutputStream[] streams = new ManagedOutputStream[count];
         int idx = 0;
         try {
             for (; idx < count; idx++) {
                 ManagedOutputStream newStream = getOutputStream(managedFiles[idx]);
                 newStream.setStreamSet(streams);
                 streams[idx] = newStream;
             }
         } catch (IOException e) {
             // cleanup
 for (int jdx = 0; jdx < idx; jdx++)
                 streams[jdx].abort();
             throw e;
         }
         return streams;
     }

     /* (non-Javadoc)
      * Instructs this manager to abort and discard a managed output stream.
      * This method should be used if any errors occur after opening a managed
      * output stream where the contents should not be saved.
      * If this output stream is part of a set, all other managed output streams in this set
      * will also be closed and aborted.
      * @param out the managed output stream
      * @see #getOutputStream(String)
      * @see #getOutputStreamSet(String[])
      */
     void abortOutputStream(ManagedOutputStream out) {
         ManagedOutputStream[] set = out.getStreamSet();
         if (set == null) {
             set = new ManagedOutputStream[] {out};
         }
         synchronized (set) {
             for (int idx = 0; idx < set.length; idx++) {
                 out = set[idx];
                 if (out.getOutputFile() == null) {
                     // this is a ReliableFileOutpuStream
 ReliableFileOutputStream rfos = (ReliableFileOutputStream) out.getOutputStream();
                     rfos.abort();
                 } else {
                     // plain FileOutputStream();
 if (out.getState() == ManagedOutputStream.ST_OPEN) {
                         try {
                             out.getOutputStream().close();
                         } catch (IOException e) {/*do nothing*/
                         }
                     }
                     out.getOutputFile().delete();
                 }
                 out.setState(ManagedOutputStream.ST_CLOSED);
             }
         }
     }

     /* (non-Javadoc)
      * Close the managed output stream and update the new content to
      * this manager. If this managed output stream is part of a set, only after closing
      * all managed output streams in the set will storage manager be updated.
      *
      * @param smos the output stream.
      * @throws IOException if an errors occur.
      * @see #getOutputStream(String)
      * @see #getOutputStreamSet(String[])
      */
     void closeOutputStream(ManagedOutputStream smos) throws IOException {
         if (smos.getState() != ManagedOutputStream.ST_OPEN)
             return;
         ManagedOutputStream[] streamSet = smos.getStreamSet();
         if (smos.getOutputFile() == null) {
             // this is a ReliableFileOutputStream
 ReliableFileOutputStream rfos = (ReliableFileOutputStream) smos.getOutputStream();
             // manage file deletes
 File file = rfos.closeIntermediateFile();
             smos.setState(ManagedOutputStream.ST_CLOSED);
             String target = smos.getTarget();
             if (streamSet == null) {
                 add(target, StorageManager.FILETYPE_RELIABLEFILE);
                 update(new String [] {smos.getTarget()}, new String [] {file.getName()});
                 ReliableFile.fileUpdated(new File(getBase(), smos.getTarget()));
             }
         } else {
             // this is a plain old file output steam
 OutputStream out = smos.getOutputStream();
             out.flush();
             try {
                 ((FileOutputStream) out).getFD().sync();
             } catch (SyncFailedException e) {/*ignore*/
             }
             out.close();
             smos.setState(ManagedOutputStream.ST_CLOSED);
             String target = smos.getTarget();
             if (streamSet == null) {
                 add(target, StorageManager.FILETYPE_STANDARD);
                 update(new String [] {target}, new String [] {smos.getOutputFile().getName()});
             }
         }

         if (streamSet != null) {
             synchronized (streamSet) {
                 //check all the streams to see if there are any left open....
 for (int idx = 0; idx < streamSet.length; idx++) {
                     if (streamSet[idx].getState() == ManagedOutputStream.ST_OPEN)
                         return; //done
 }
                 //all streams are closed, we need to update storage manager
 String [] targets = new String [streamSet.length];
                 String [] sources = new String [streamSet.length];
                 for (int idx = 0; idx < streamSet.length; idx++) {
                     smos = streamSet[idx];
                     targets[idx] = smos.getTarget();
                     File outputFile = smos.getOutputFile();
                     if (outputFile == null) {
                         // this is a ReliableFile
 add(smos.getTarget(), StorageManager.FILETYPE_RELIABLEFILE);
                         ReliableFileOutputStream rfos = (ReliableFileOutputStream) smos.getOutputStream();
                         File file = rfos.closeIntermediateFile(); //multiple calls to close() ok
 sources[idx] = file.getName();
                         ReliableFile.fileUpdated(new File(getBase(), smos.getTarget()));
                     } else {
                         add(smos.getTarget(), StorageManager.FILETYPE_STANDARD);
                         sources[idx] = outputFile.getName();
                     }
                 }
                 update(targets, sources);
             }
         }
     }
 }
